﻿// Original author contact info: Owen Emlen (owene_1998@yahoo.com)
// Note: other individuals may also have contributed to this code
// Project hosted on CodePlex.com as of 1/10/2009 at http://www.codeplex.com/EmlenMud
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Reflection;
using BrainTechLLC.ThreadSafeObjects;
using System.Diagnostics;

namespace BrainTechLLC
{
    public static class ReflectionHelpers
    {
        /// <summary>
        /// If methodInfo is null, return null
        /// Otherwise, invoke it using this = thisObject with parameters 'parameters'
        /// Return the return value of the method, converted to a boolean
        /// </summary>
        /// <param name="methodInfo"></param>
        /// <param name="thisObject"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static bool? BoolInvokeIfNotNull(this MethodInfo methodInfo, object thisObject, params object[] parameters)
        {
            if (methodInfo == null)
                return null;

            return Convert.ToBoolean(methodInfo.Invoke(thisObject, parameters));
        }

        /// <summary>
        /// If methodInfo is null, return null
        /// Otherwise, invoke it using this = thisObject with parameters 'parameters'
        /// Return the return value of the method
        /// </summary>
        /// <param name="methodInfo"></param>
        /// <param name="thisObject"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static object InvokeIfNotNull(this MethodInfo methodInfo, object thisObject, params object[] parameters)
        {
            if (methodInfo == null)
                return null;

            return methodInfo.Invoke(thisObject, parameters);
        }

        /// <summary>
        /// Reads all attributes of type T1 associated with MethodInfo t
        /// </summary>
        /// <typeparam name="T1"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T1[] ReadAttribute<T1>(this MethodInfo methodInfo)
            where T1 : class, IReadableAttribute
        {
            ThreadSafeLookup<Type, object> lookup = Lookups.MethodInfoAttributeLookup.Get(methodInfo);
            Type t1Type = typeof(T1);

            object o;
            if (lookup.TryGetValue(t1Type, out o))
            {
                if (o == null)
                    return null;

                return o as T1[];
            }

            object[] objs = methodInfo.GetCustomAttributes(typeof(T1), true);

            if (objs != null && objs.Length > 0)
            {
                T1[] results = new T1[objs.Length];

                for (int n = 0; n < objs.Length; n++)
                {
                    results[n] = objs[n] as T1;
                }

                lookup.AddOrSet(t1Type, results);
                return results;
            }

            lookup.AddOrSet(t1Type, null);
            return null;
        }

        /// <summary>
        /// Reads all attributes of type T1 associated with PropertyInfo propInfo
        /// </summary>
        /// <typeparam name="T1"></typeparam>
        /// <param name="propInfo"></param>
        /// <returns></returns>
        public static T1[] ReadAttribute<T1>(this PropertyInfo propInfo)
            where T1 : class, IReadableAttribute
        {
            ThreadSafeLookup<Type, object> lookup = Lookups.PropInfoAttributeLookup.Get(propInfo);
            Type t1Type = typeof(T1);

            object o;
            if (lookup.TryGetValue(t1Type, out o))
            {
                if (o == null)
                    return null;

                return o as T1[];
            }

            object[] objs = propInfo.GetCustomAttributes(typeof(T1), true);
            if (objs != null && objs.Length > 0)
            {
                T1[] results = new T1[objs.Length];

                for (int n = 0; n < objs.Length; n++)
                    results[n] = objs[n] as T1;

                lookup.AddOrSet(t1Type, results);
                return results;
            }

            lookup.AddOrSet(t1Type, null);
            return null;
        }

        /// <summary>
        /// Reads all attributes of type T1 associated with class classType
        /// </summary>
        /// <typeparam name="T1"></typeparam>
        /// <param name="classType"></param>
        /// <returns></returns>
        public static T1[] ReadAttribute<T1>(this Type classType)
            where T1 : class, IReadableAttribute
        {
            ThreadSafeLookup<Type, object> lookup = Lookups.ClassInfoAttributeLookup.Get(classType);
            Type t1Type = typeof(T1);

            object o;
            if (lookup.TryGetValue(t1Type, out o))
            {
                if (o == null)
                    return null;

                return o as T1[];
            }

            object[] objs = classType.GetCustomAttributes(typeof(T1), true);
            if (objs != null && objs.Length > 0)
            {
                T1[] results = new T1[objs.Length];

                for (int n = 0; n < objs.Length; n++)
                    results[n] = objs[n] as T1;

                lookup.AddOrSet(t1Type, results);
                return results;
            }

            lookup.AddOrSet(t1Type, null);
            return null;
        }

        /// <summary>
        /// Reads all method infos that have attribute type t
        /// </summary>
        /// <typeparam name="T1"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static List<MethodInfo> ReadMethodInfoWithCustomAttribute<T1>(this Type t)
            where T1 : class, IReadableAttribute
        {
            // TODO: Cache this info?
            List<MethodInfo> results = new List<MethodInfo>();

            // TODO: private, etc?
            MethodInfo[] methods = t.GetMethods();
            foreach (MethodInfo method in methods)
            {
                T1[] attributes = method.ReadAttribute<T1>();

                if (attributes != null && attributes.Length > 0)
                {
                    results.Add(method);
                }
            }

            return results;
        }

        /// <summary>
        /// Reads all property infos that have attribute type t
        /// </summary>
        /// <typeparam name="T1"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static List<PropertyInfo> ReadPropertyInfoWithCustomAttribute<T1>(this Type t)
            where T1 : class, IReadableAttribute
        {
            List<PropertyInfo> results = new List<PropertyInfo>();

            PropertyInfo[] props = t.GetProperties();
            foreach (PropertyInfo prop in props)
            {
                T1[] attributes = prop.ReadAttribute<T1>();

                if (attributes != null && attributes.Length > 0)
                {
                    results.Add(prop);
                }
            }

            return results;
        }

        /// <summary>
        /// Reads the attribute of type T1 on type t.  Returns its value as type T2
        /// </summary>
        /// <typeparam name="T1"></typeparam>
        /// <typeparam name="T2"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T2 ReadPropertyValue<T1, T2>(this Type t)
            where T1 : IReadableAttribute
        {
            object[] objs = t.GetCustomAttributes(typeof(T1), true);

            if (objs != null && objs.Length > 0)
            {
                T1 attr = (T1)objs[0];
                return (T2)attr.Value;
            }
            else { return default(T2); }
        }

#if NO_SILVERLIGHT
#if TYPE_ID_PROPERTY_MAP

        internal static readonly ThreadSafeLookup<Type, PropertyDescriptor> _typeIDPropertyMap = new ThreadSafeLookup<Type, PropertyDescriptor>();

        public static PropertyDescriptor IDProperty(this Type type)
        {
            string propName; PropertyDescriptor prop;
            if (_typeIDPropertyMap.TryGetValue(type, out prop)) { return prop; }
            propName = type.ReadPropertyValue<IDProperty, string>();
            prop = TypeDescriptor.GetProperties(type)[propName];
            _typeIDPropertyMap.AddOrSet(type, prop);
            return prop;
        }

#endif
#endif

        /// <summary>
        /// Finds the empty constructor in the collection, or returns null if no constructors take 0 arguments
        /// </summary>
        /// <param name="constructors"></param>
        /// <returns></returns>
        public static ConstructorInfo FindEmptyConstructor(this IEnumerable<ConstructorInfo> constructors)
        {
            foreach (var c in constructors)
            {
                var param = c.GetParameters();

                if (param == null || param.Length == 0)
                    return c;
            }

            return null;
        }

        /// <summary>
        /// Makes a copy of obj at a given depth.  Not bulletproof - just take 1
        /// obj type must have a default constructor (no args) OR must be an array type
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="depth"></param>
        /// <param name="currentDepth"></param>
        /// <returns></returns>
        public static object MakeCopy(this object obj, int depth, int currentDepth)
        {
            Type typeOrig = obj.GetType();
            object newObject = null;

            try
            {
                var constructors = typeOrig.GetConstructors();
                var emptyConstructor = constructors.FindEmptyConstructor();

                if (emptyConstructor != null)
                {
                    newObject = Activator.CreateInstance(typeOrig, null);
                }
                else
                {
                    if (typeOrig.IsArray)
                    {
                        int arrayRank = typeOrig.GetArrayRank();

                        if (arrayRank > 0)
                        {
                            object[] ranks = new object[arrayRank];
                            Type[] types = new Type[arrayRank];
                            Array a = obj as Array;

                            for (int n = 0; n < arrayRank; n++)
                            {
                                int rank = a.GetUpperBound(n);
                                ranks[n] = rank + 1;
                                types[n] = typeof(int);
                            }

                            var c = typeOrig.GetConstructor(types);
                            newObject = c.Invoke(ranks);

                            Array aCopy = newObject as Array;
                            Array.Copy(a, aCopy, a.Length);
                        }
                        else
                        {
                            Console.WriteLine("Unhandled " + typeOrig.Name);
                        }
                    }
                    else
                    {
                        Console.WriteLine("Unhandled " + typeOrig.Name);
                    }
                }

                if (newObject == null)
                    return null;

                var props = obj.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance);

                foreach (var prop in props)
                {
                    if (!prop.CanRead || !prop.CanWrite)
                    {
                        continue;
                    }

                    object oSource = prop.GetValue(obj, null);

                    if (oSource != null)
                    {
                        Type oSourceType = oSource.GetType();
                        object oCopiedValue = oSource;

                        if (currentDepth < depth && oSourceType.IsClass)
                        {
                            oCopiedValue = MakeCopy(oSource, depth, currentDepth + 1);

                            if (oCopiedValue == null)
                                oCopiedValue = oSource;
                        }

                        prop.SetValue(newObject, oCopiedValue, null);
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
                Debug.Assert(false);
                Console.WriteLine(ex.ToString());
            }

            return newObject;
        }

        /// <summary>
        /// Gets the value of a property on object 'obj'.  If value of property is null or 0, find a method marked with the attribute [PopulatesProperty("PropertyName")] 
        /// (This method must return an object of type T2)
        /// Run the method and set the property value to the result.
        /// Returns the (possibly newly populated) value of propertyName
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="T2"></typeparam>
        /// <param name="obj"></param>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        public static T2 GetValue<T, T2>(this T obj, string propertyName) where T : class
        {

            PropertyInfo pi = typeof(T).GetProperty(propertyName);
            return obj.GetValue<T, T2>(pi, propertyName);
        }

        public static T2 GetValue<T, T2>(this T obj, PropertyInfo pi) where T : class
        {
            return obj.GetValue<T, T2>(pi, pi.Name);
        }

        public static T2 GetValue<T, T2>(this T obj, PropertyInfo pi, string propertyName) where T : class
        {
            T2 result = (T2)pi.GetValue(obj, null);

            if (default(T2).Equals(result))
            {
                MethodInfo[] mis = typeof(T).GetMethods();
                foreach (MethodInfo mi in mis)
                {
                    object[] objs = mi.GetCustomAttributes(typeof(PopulatesProperty), true);
                    if (objs != null && objs.Length > 0)
                    {
                        PopulatesProperty pp = (PopulatesProperty)objs[0];
                        if (pp.PopulatesPropertyName.Equals(propertyName, StringComparison.OrdinalIgnoreCase))
                        {
                            object ret = mi.Invoke(obj, null);
                            pi.SetValue(obj, (T2)ret, null);
                            return (T2)ret;
                        }
                    }
                }
            }

            return result;
        }
    }
}